(function() {
  var events = require('events');
  var moment = require('moment');
  var util = require('util');
  var ElasticSearchClient = require('elasticsearchclient');
  var Logger = require(__dirname + '/../log/logger');
  var JsonReader = require(__dirname + '/../util/jsonReader');
  var searchUtil = require('./util');
  var searchSetting = JsonReader.read(__dirname + '/../../config/search/searchSetting.json');
  var config = JsonReader.read(__dirname + '/../../config/search/searchConfig.json');

  var ES_SERVER_OPTIONS = {
    host : config.elServerHost,
    port : config.elServerPort
  };
  var SORT_TYPES = [ 'asc', 'desc' ];
  var MAX_FETCH_SIZE = config.maxFetchSize;

  var IndexSearcher = function() {
    this.logger = Logger.getLogger();
    this.searchClient = new ElasticSearchClient(ES_SERVER_OPTIONS);
    events.EventEmitter.call(this);
  };

  util.inherits(IndexSearcher, events.EventEmitter);

  IndexSearcher.prototype.constructor = IndexSearcher;
  IndexSearcher.prototype.searchClient = null;
  IndexSearcher.prototype.logger = null;

  IndexSearcher.prototype.search = function(type, queryObj, callback) {
    var self = this;

    // validate type
    if (!searchUtil.isValidType(type)) {
      var errorResult = searchUtil.composeErrorResult('type should be in [user, account, event].', this.logger);
      if (typeof callback == 'function') {
        callback(null, errorResult);
      } else {
        self.emit('finish', errorResult);
      }
      return;
    }

    // parse queryObj to elasticsearch query
    try {
      var elQueryObj = _parseQueryObj(type, queryObj);
    } catch (e) {
      var errorResult = searchUtil.composeErrorResult(e.message, this.logger);
      if (typeof callback == 'function') {
        callback(null, errorResult);
      } else {
        self.emit('finish', errorResult);
      }
      return;
    }

    // search document
    this.searchClient.search(config.indexName, type, elQueryObj, function(err, data) {
      if (err) {
        if (typeof callback == 'function') {
          callback(err, null);
        } else {
          self.emit('error', err);
        }
      } else {
        var result = _parseQueryResult(data);
        if (typeof callback == 'function') {
          callback(null, result);
        } else {
          self.emit('finish', result);
        }
      }
    });

    return this;
  };

  var boolOperators = {
    '$and' : function(boolQueryObjs, queryObj) {
      queryObj['bool'] = {
        'must' : boolQueryObjs
      };
    },
    '$or' : function(boolQueryObjs, queryObj) {
      queryObj['bool'] = {
        'should' : boolQueryObjs
      };
    }
  };

  var logicOperators = {
    '$equal' : function(field, value, queryObj) {
      queryObj['term'] = {};
      var fieldName = field + '._untokenized';
      queryObj['term'][fieldName] = value.toLowerCase();
    },
    '$startWith' : function(field, value, queryObj) {
      queryObj['prefix'] = {};
      var fieldName = field + '._untokenized';
      queryObj['prefix'][fieldName] = value.toLowerCase();
    },
    '$endWith' : function(field, value, queryObj) {
      var containQuery = {
        'query_string' : {
          'fields' : [],
          'query' : ''
        }
      };
      var fieldName = field + '._untokenized';
      containQuery['query_string']['fields'].push(fieldName);
      containQuery['query_string']['query'] = '*' + value.toLowerCase();
      queryObj['query'] = containQuery;
    },
    '$in' : function(field, value, queryObj) {
      queryObj['terms'] = {};
      var fieldName = field + '._untokenized';
      var lowerCaseValue = [];
      value.forEach(function(item) {
        lowerCaseValue.push(item.toLowerCase());
      });
      queryObj['terms'][fieldName] = lowerCaseValue;
    },
    '$contain' : function(field, value, queryObj) {
      var containQuery = {
        'query_string' : {
          'fields' : [],
          'query' : ''
        }
      };
      var fieldName = field + '._untokenized';
      containQuery['query_string']['fields'].push(fieldName);
      containQuery['query_string']['query'] = '*' + value.toLowerCase() + '*';
      queryObj['query'] = containQuery;
    },
    '$gt' : function(field, value, queryObj) {
      queryObj['range'] = {};
      queryObj['range'][field] = {};
      queryObj['range'][field]['gt'] = value;
    },
    '$lt' : function(field, value, queryObj) {
      queryObj['range'] = {};
      queryObj['range'][field] = {};
      queryObj['range'][field]['lt'] = value;
    }
  };

  function _parseQueryResult(queryResultStr) {
    var queryResult = JSON.parse(queryResultStr);
    var result = {};
    result['data'] = {};
    result['data']['total'] = queryResult.hits['total'];
    result['data']['results'] = [];
    queryResult['hits']['hits'].forEach(function(rowData) {
      result['data']['results'].push(rowData['_source']);
    });
    return result;
  }

  /**
   * Convert queryObj to elasticsearch format query string
   * 
   * @param queryObj
   */
  function _parseQueryObj(type, queryObj) {

    if (!_isObject(queryObj)) {
      throw new Error('Invalid json format.');
    }

    if (!queryObj.hasOwnProperty('start')) {
      throw new Error('No start found.');
    }
    if (!queryObj.hasOwnProperty('fetch')) {
      throw new Error('No fetch found.');
    }
    if (!queryObj.hasOwnProperty('filter')) {
      throw new Error('No filter found.');
    }

    var finalELQueryObj = {};
    var hasSort = false;
    for ( var segment in queryObj) {
      if (segment == 'sort') {
        if (queryObj[segment].length > 0) {
          hasSort = true;
          var sorts = [];
          queryObj[segment].forEach(function(oneSort) {
            for ( var field in oneSort) {
              if (!_isValidSortField(type, field)) {
                throw new Error('Unsupport sort field: ' + field);
              }
              var sortType = oneSort[field];
              if (!_isValidSortType(sortType)) {
                throw new Error('Invalid sort type: ' + sortType);
              }

              var sort = {};
              var fieldName = field + '._untokenized';
              sort[fieldName] = {
                'order' : sortType
              };
              sorts.push(sort);
            }
          });
          finalELQueryObj['sort'] = sorts;
        }
      } else if (segment == 'start') {
        var start = queryObj[segment];
        if (!_isValidInt(start, 1)) {
          throw new Error('start must be equal or greater than 1.');
        }
        // elasticsearch is from 0, so need minus 1
        finalELQueryObj['from'] = start - 1;
      } else if (segment == 'fetch') {
        var fetch = queryObj[segment];
        if (!_isValidInt(fetch, 1, MAX_FETCH_SIZE)) {
          throw new Error(util.format('fetch must be (0, %d].', MAX_FETCH_SIZE));
        }
        finalELQueryObj['size'] = fetch;
      } else if (segment == 'filter') {
        var filterObj = queryObj[segment];
        if (!_isObject(filterObj)) {
          throw new Error('Invalid filter.');
        }
        var keys = Object.keys(filterObj);
        if (keys.length != 1) {
          throw new Error('Invalid filter.');
        }
        var key = keys[0];
        var filterQuery = {};
        if (!_isBooleanOperator(key)) {
          throw new Error('Invalid filter.');
        }
        var boolQueries = _parseBoolQuery(type, filterObj[key]);
        boolOperators[key](boolQueries, filterQuery);
        finalELQueryObj['query'] = {};
        finalELQueryObj['query']['filtered'] = {};
        finalELQueryObj['query']['filtered']['filter'] = filterQuery;
      } else {
        throw new Error("Invalid segment: " + segment);
      }
    }

    if (!hasSort) {
      // no sort specified, sort result by default sort field
      var defaultSortField = _getDefaultSortField(type) + '._untokenized';
      finalELQueryObj['sort'] = {};
      finalELQueryObj['sort'][defaultSortField] = {
        'order' : 'asc'
      };
    }

    return finalELQueryObj;
  }

  function _parseBoolQuery(type, queryObj) {
    if (!_isObject(queryObj)) {
      throw new Error('Invalid filter.');
    }

    if (queryObj.length == 0) {
      throw new Error('Invalid filter.');
    }

    var boolQuery = [];
    queryObj.forEach(function(criteria) {
      if (!_isObject(criteria)) {
        throw new Error('Invalid filter.');
      }

      var keys = Object.keys(criteria);
      if (keys.length != 1) {
        throw new Error('Invalid filter.');
      }
      var key = keys[0];
      var query;
      if (_isBooleanOperator(key)) {
        var logicQueries = _parseBoolQuery(type, criteria[key]);
        query = {};
        boolOperators[key](logicQueries, query);
      } else {
        query = _parseLogicQuery(type, criteria);
      }
      boolQuery.push(query);
    });

    return boolQuery;
  }

  function _parseLogicQuery(type, queryObj) {
    if (!_isObject(queryObj)) {
      throw new Error('Invalid filter.');
    }
    var keys = Object.keys(queryObj);
    if (keys.length != 1) {
      throw new Error('Invalid filter.');
    }

    var field = keys[0];
    // key is field name, the pattern could be {field : 'value'} or {field :
    // {'$starWith' : 'value'}}
    if (!_isValidFilterField(type, field)) {
      throw new Error('Unsupport filter field found: ' + field);
    }
    
    var operatorAndValue = queryObj[field];
    if (!_isObject(operatorAndValue)) {
      throw new Error('Invalid filter.');
    }
    
    var operator = Object.keys(operatorAndValue)[0];
    if (!_isLogicOperator(operator)) {
      throw new Error("Invalid operator found: " + operator);
    }
    // validation if operator is support for this field
    if (!_isValidFilterOperator(type, field, operator)) {
    	throw new Error('Unsupport operator of filter field: ' + field + ", operator: " + operator);
    }
    
    var value = operatorAndValue[operator];
    // validation if value is valid for this field
    if (!_isValidFilterValue(type, field, value)) {
      throw new Error('Invalid value of filter field: ' + field + ", value: " + value);
    }

    var elQuery = {};
    logicOperators[operator](field, value, elQuery);
    return elQuery;
  }

  function _isBooleanOperator(opt) {
    return boolOperators.hasOwnProperty(opt);
  }

  function _isLogicOperator(opt) {
    return logicOperators.hasOwnProperty(opt);
  }

  function _isObject(obj) {
    return (typeof obj == 'object');
  }

  function _isValidSortField(type, fieldName) {
    var fields = searchSetting[type];
    if (fields) {
      var field = fields[fieldName];
      if (field) {
        return field['sortable'];
      } else {
        return false;
      }
    }
    return false;
  }

  function _isValidSortType(sortType) {
    return SORT_TYPES.indexOf(sortType) != -1;
  }

  function _isValidFilterField(type, fieldName) {
    var fields = searchSetting[type];
    if (fields) {
      var field = fields[fieldName];
      if (field) {
        return field['filterble'];
      } else {
        return false;
      }
    }
    return false;
  }

  function _isValidFilterOperator(type, fieldName, operator) {
    var fields = searchSetting[type];
    if (fields) {
      var field = fields[fieldName];
      if (field) {
        var operators = field["operators"];
        if (operators) {
          if (operators.indexOf(operator) != -1) {
            return true;
          }
          return false;
        }
      }
    }
    return true;
  }

  function _isValidFilterValue(type, fieldName, value) {
    var fields = searchSetting[type];
    if (fields) {
      var field = fields[fieldName];
      if (field) {
        var validations = field["validations"];
        if (validations) {
          for ( var validation in validations) {
            if (validations[validation] === 'date') {
              return moment(value).isValid();
            }
          }
        }
      }
    }
    return true;
  }

  function _isValidInt(strValue, minValue, maxValue) {
    var intValue = parseInt(strValue);
    if (isNaN(intValue)) {
      return false;
    }
    if (minValue && intValue < minValue) {
      return false;
    }
    if (maxValue && intValue > maxValue) {
      return false;
    }
    return true;
  }

  function _getDefaultSortField(type) {
    var fields = searchSetting[type];
    for ( var field in fields) {
      if (fields[field]['isDefaultSort']) {
        return field;
      }
    }
    throw new Error('No default sort field found.');
  }

  module.exports = IndexSearcher;
})();
